package cn.ges.redis;

import lombok.extern.slf4j.Slf4j;

/**
 * redis分布式锁
 *
 * @author gespent@163.com
 * @date 2019/1/17
 * @see <a href="http://redis.io/commands/setnx">Redis Documentation: SETNX</a>
 */
@Slf4j
public class Lock {
    /**
     * 用于记录执行次数
     */
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    /**
     * 根据key 强制获得一个锁
     *
     * @param key     业务KEY ->需要外部使用唯一业务锁KEY
     * @param timeOut 锁最长锁定时间 毫秒 最少为1000ms->不足将自动补齐
     * @return 0代表失败 其他情况返回获得锁的锁定到期时间(System.currentTimeMillis()值)
     */
    public static long forceLock(String key, long timeOut) {
        long value = System.currentTimeMillis() + timeOut + 1;
        if (RedisUtil.set(key, value, timeOut / 1000)) {
            return value;
        } else {
            return 0L;
        }
    }

    /**
     * 根据KEY尝试获取一个锁
     * 当尝试获取锁失败后，在waitTime毫秒后将再次尝试获取锁
     *
     * @param key         业务KEY->因为有递归 内部对key没有做处理 需要外部使用唯一业务锁KEY
     * @param timeOut     锁最长锁定时间 毫秒 最少为1000ms->不足将自动补齐
     * @param waitTime    每次重新等待锁时间 毫秒 最少为0
     * @param maxAttempts 最多尝试次数 等于0或者负数时 会直接返回0
     * @return 0代表失败 其他情况返回获得锁的锁定到期时间(System.currentTimeMillis()值)
     */
    public static long lock(String key, long timeOut, long waitTime, int maxAttempts) {
        if (timeOut < 1000) {
            timeOut = 1000;
        }
        if (threadLocal.get() >= maxAttempts) {
            threadLocal.set(0);
            return 0;
        }
        threadLocal.set(threadLocal.get() + 1);
        long value = System.currentTimeMillis() + timeOut + 1;
        // 尝试直接获得锁
        if (RedisUtil.setIfNotExists(key, value, timeOut)) {
            threadLocal.set(0);
            return value;
        }
        // 锁被占用的情况
        // 获取过期时间
        Object o = RedisUtil.get(key);
        if (o == null) {
            return Lock.lock(key, timeOut, waitTime, maxAttempts);
        } else {
            long now = System.currentTimeMillis();
            // 如果当前时间大于当前锁的过期时间,说明锁过期
            if (now > (long) o) {
                // 准备插入的锁的值
                long set = System.currentTimeMillis() + timeOut + 1;
                // 此处result可能2种情况 1:o(过期的时间戳) 2：其他客户端插入时间
                Object result = RedisUtil.getSet(key, set);
                if (result == null) {
                    threadLocal.set(0);
                    return set;
                }
                if ((long) o == (long) result) {
                    threadLocal.set(0);
                    return set;
                } else {
                    // 重新尝试获得锁
                    return Lock.lock(key, timeOut, waitTime, maxAttempts);
                }
            } else {
                // 如果maxAttempts=1 直接获取锁失败
                if (maxAttempts == 1) {
                    threadLocal.set(0);
                    return 0L;
                }
                // 锁被占用 且没有过期 等待
                try {
                    if (waitTime > 0) {
                        Thread.sleep(waitTime);
                    }
                } catch (InterruptedException e) {
                    threadLocal.set(0);
                    return 0L;
                }
                return Lock.lock(key, timeOut, waitTime, maxAttempts);
            }
        }
    }

    /**
     * 业务完成后 根据key 进行解锁
     *
     * @param key  业务key
     * @param time 加锁时获取到的锁失效时间
     */
    public static void unlock(String key, Long time) {
        // 获得当前加锁对象
        Object o = RedisUtil.get(key);
        if (o == null) {
            return;
        }
        // 当前锁时间 等于失效时间,进行删除 否则不进行操作
        if ((long) o == time) {
            // 当前锁仍未失效,删除当前锁
            RedisUtil.del(key);
        }
    }
}
